/**
 * 
 */
package org.swing.utility.common.file;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;

import org.swing.utility.common.clazz.CloserUtil;
import org.swing.utility.common.exception.SAMException;

/**
 * @author lqnhu
 *
 */
public class FastLineReader implements Closeable {
	private InputStream in;
	private byte[] fileBuffer = new byte[512000];
	// Next byte to read in fileBuffer
	private int nextByte = 0;
	// Number of bytes in fileBuffer
	private int numBytes = 0;
	private boolean atEof;

	public FastLineReader(final InputStream in) {
		this.in = in;
		ensureBufferNotEmpty();
	}

	/**
	 * @return true if input is exhausted
	 */
	public boolean eof() {
		return atEof;
	}

	/**
	 * @return peeks at the next byte in the stream and returns true if it is CR
	 *         or LF. Returns false if EOF or next byte is not CR or LF.
	 */
	public boolean atEoln() {
		return ensureBufferNotEmpty()
				&& (fileBuffer[nextByte] == '\n' || fileBuffer[nextByte] == '\r');
	}

	/**
	 * Advance over any EOLN chars (CR or LF)
	 * 
	 * @return true if saw one or more CR or LFs
	 */
	public boolean skipNewlines() {
		boolean sawEoln = false;
		while (atEoln()) {
			sawEoln = true;
			++nextByte;
		}
		return sawEoln;
	}

	public void close() {
		CloserUtil.close(in);
		in = null;
		fileBuffer = null;
	}

	/**
	 * @return Next byte from the input. Do not call if at EOF.
	 */
	public byte getByte() {
		final byte ret = peekByte();
		++nextByte;
		ensureBufferNotEmpty();
		return ret;
	}

	/**
	 * @return Next byte from the input, without advancing over that byte. Do
	 *         not call if at EOF.
	 */
	public byte peekByte() {
		if (eof()) {
			throw new IllegalStateException("Cannot getByte() if EOF.");
		}
		return fileBuffer[nextByte];
	}

	/**
	 * Read from input until input is exhausted, EOLN is seen, or output buffer
	 * is filled
	 * 
	 * @param outputBuffer
	 *            where to put bytes read
	 * @param startOutputIndex
	 *            where to start putting bytes read
	 * @return number of bytes read
	 */
	public int readToEndOfOutputBufferOrEoln(final byte[] outputBuffer,
			final int startOutputIndex) {
		boolean sawNewline;
		int totalGrabbed = 0;
		do {
			if (!ensureBufferNotEmpty()) {
				break;
			}
			final int startInputIndex = nextByte;
			sawNewline = advanceToEobOrEoln();
			int lengthOfChunk = nextByte - startInputIndex;
			// Roll back if went past the amount that can be stored in the
			// output buffer.
			// Assumption is that lines are relatively short so this won't
			// happen very often.
			if (lengthOfChunk > outputBuffer.length
					- (startOutputIndex + totalGrabbed)) {
				lengthOfChunk = outputBuffer.length
						- (startOutputIndex + totalGrabbed);
				nextByte = startInputIndex + lengthOfChunk;
			}
			System.arraycopy(fileBuffer, startInputIndex, outputBuffer,
					startOutputIndex + totalGrabbed, lengthOfChunk);
			totalGrabbed += lengthOfChunk;
		} while (!sawNewline
				&& totalGrabbed < outputBuffer.length - startOutputIndex);
		ensureBufferNotEmpty();
		return totalGrabbed;
	}

	/**
	 * Advance nextByte to end of currently-buffered input or to line terminator
	 * 
	 * @return true if saw a line terminator
	 */
	private boolean advanceToEobOrEoln() {
		while (nextByte < numBytes) {
			if (atEoln()) {
				return true;
			}
			++nextByte;
		}
		return false;
	}

	/**
	 * Ensure that fileBuffer has at least one byte available in it. Potentially
	 * wipes out what is in fileBuffer so everything from
	 * fileBuffer[0..nextByte] should already have been pulled out.
	 * 
	 * @return false if EOF, else true
	 */
	private boolean ensureBufferNotEmpty() {
		try {
			if (nextByte < numBytes) {
				return true;
			}
			nextByte = 0;
			numBytes = in.read(fileBuffer);
			atEof = (numBytes < 1);
			return !atEof;
		} catch (IOException e) {
			throw new SAMException("Exception reading InputStream", e);
		}
	}
}
